package org.rsbot.script.methods;

import org.rsbot.script.util.Timer;
import org.rsbot.script.wrappers.RSLocatable;

import java.awt.event.KeyEvent;

/**
 * Camera related operations.
 *
 * @author Hyperion
 */
public class Camera extends MethodProvider {
    Camera(final MethodContext ctx) {
        super(ctx);
    }

    /**
     * Turns within a few degrees to a specific RSLocatable.
     *
     * @param locatable Locatable to turn to.
     * @param dev       Maximum deviation from the angle to the locatable.
     */
    public void turnTo(final RSLocatable locatable, final int dev) {
        int angle = getAngleOf(locatable);
        angle = random(angle - dev, angle + dev + 1);
        setAngle(angle);
    }

    /**
     * Turns to a specific RSLocatable.
     *
     * @param locatable Locatable to turn to.
     */
    public void turnTo(final RSLocatable locatable) {
        turnTo(locatable, 0);
    }

    /**
     * Sets the altitude to max or minimum.
     *
     * @param up True to go up. False to go down.
     * @return <tt>true</tt> if the altitude was changed.
     */
    public boolean setPitch(final boolean up) {
        return up ? setPitch(100) : setPitch(0);
    }

    /**
     * Set the camera to a certain percentage of the maximum pitch. Don't rely
     * on the return value too much - it should return whether the camera was
     * successfully set, but it isn't very accurate near the very extremes of
     * the height.
     * <p/>
     * <p/>
     * This also depends on the maximum camera angle in a region, as it changes
     * depending on situation and surroundings. So in some areas, 68% might be
     * the maximum altitude. This method will do the best it can to switch the
     * camera altitude to what you want, but if it hits the maximum or stops
     * moving for any reason, it will return.
     * <p/>
     * <p/>
     * <p/>
     * Mess around a little to find the altitude percentage you like. In later
     * versions, there will be easier-to-work-with methods regarding altitude.
     *
     * @param percent The percentage of the maximum pitch to set the camera to.
     * @return true if the camera was successfully moved; otherwise false.
     */
    public boolean setPitch(final int percent) {
        int currentPitch;
        while ((currentPitch = getPitch()) != percent) {
            if (currentPitch < percent) {
                methods.inputManager.pressKey((char) KeyEvent.VK_UP);
                while ((currentPitch = getPitch()) < percent) {
                    sleep(random(50, 100));
                    if (currentPitch == getPitch()) {
                        break;
                    }
                }
                methods.inputManager.releaseKey((char) KeyEvent.VK_UP);
                continue;
            } else if (currentPitch > percent) {
                methods.inputManager.pressKey((char) KeyEvent.VK_DOWN);
                while ((currentPitch = getPitch()) > percent) {
                    sleep(random(50, 100));
                    if (currentPitch == getPitch()) {
                        break;
                    }
                }
                methods.inputManager.releaseKey((char) KeyEvent.VK_DOWN);
                continue;
            }
            break;
        }
        return getPitch() == percent;
    }

    /**
     * Moves the camera in a random direction for a given time.
     *
     * @param timeOut The maximum time in milliseconds to move the camera for.
     */
    public void moveRandomly(final int timeOut) {
        final Timer timeToHold = new Timer(timeOut);
        final int highest = random(75, 100);
        final int lowest = random(0, 25);
        final int vertical = Math.random() < Math.random() ? KeyEvent.VK_UP : KeyEvent.VK_DOWN;
        final int horizontal = Math.random() < Math.random() ? KeyEvent.VK_LEFT : KeyEvent.VK_RIGHT;
        boolean verticalKeyDown = false;
        boolean horizontalKeyDown = false;
        if (random(0, 10) < 8) {
            methods.inputManager.pressKey((char) vertical);
            verticalKeyDown = true;
        }
        if (random(0, 10) < 8) {
            methods.inputManager.pressKey((char) horizontal);
            horizontalKeyDown = true;
        }
        while (timeToHold.isRunning() && (verticalKeyDown || horizontalKeyDown)) {
            if (getPitch() >= highest && vertical == KeyEvent.VK_UP ||
                    getPitch() <= lowest && vertical == KeyEvent.VK_DOWN) {
                methods.inputManager.releaseKey((char) vertical);
                verticalKeyDown = false;
            }
            sleep(10);
        }
        if (verticalKeyDown) {
            methods.inputManager.releaseKey((char) vertical);
        }
        if (horizontalKeyDown) {
            methods.inputManager.releaseKey((char) horizontal);
        }
    }

    /**
     * Rotates the camera to a specific angle in the closest direction.
     *
     * @param degrees The angle to rotate to.
     */
    public void setAngle(final int degrees) {
        if (getAngleTo(degrees) > 5) {
            methods.inputManager.pressKey((char) KeyEvent.VK_LEFT);
            while (getAngleTo(degrees) > 5 && methods.game.getClientState() == 11) {
                sleep(10);
            }
            methods.inputManager.releaseKey((char) KeyEvent.VK_LEFT);
        } else if (getAngleTo(degrees) < -5) {
            methods.inputManager.pressKey((char) KeyEvent.VK_RIGHT);
            while (getAngleTo(degrees) < -5 && methods.game.getClientState() == 11) {
                sleep(10);
            }
            methods.inputManager.releaseKey((char) KeyEvent.VK_RIGHT);
        }
    }

    /**
     * Rotates the camera to the specified cardinal direction.
     *
     * @param direction The char direction to turn the map. char options are w,s,e,n
     *                  and defaults to north if character is unrecognized.
     */
    public void setCompass(final char direction) {
        switch (direction) {
            case 'n':
                setAngle(359);
                break;
            case 'w':
                setAngle(89);
                break;
            case 's':
                setAngle(179);
                break;
            case 'e':
                setAngle(269);
                break;
            default:
                break;
        }
    }

    /**
     * Uses the compass component to set the camera to face north.
     */
    public void setNorth() {
        methods.interfaces.getComponent(methods.gui.getCompass().getID()).doClick();
    }

    /**
     * Returns the camera angle at which the camera would be facing a certain
     * RSLocatable
     *
     * @param locatable the RSLocatable
     * @return The angle
     */
    public int getAngleOf(final RSLocatable locatable) {
        final int a = (methods.calc.angleToTile(locatable.getLocation()) - 90) % 360;
        return a < 0 ? a + 360 : a;
    }

    /**
     * Returns the angle between the current camera angle and the given angle in
     * degrees.
     *
     * @param degrees The target angle.
     * @return The angle between the who angles in degrees.
     */
    public int getAngleTo(final int degrees) {
        int ca = getAngle();
        if (ca < degrees) {
            ca += 360;
        }
        int da = ca - degrees;
        if (da > 180) {
            da -= 360;
        }
        return da;
    }

    /**
     * Returns the current compass orientation in degrees, with North at 0,
     * increasing counter-clockwise to 360.
     *
     * @return The current camera angle in degrees.
     */
    public int getAngle() {
        // the client uses fixed point radians 0 - 2^14
        // degrees = yaw * 360 / 2^14 = yaw / 45.5111...
        return (int) (methods.client.getCameraYaw() / 45.51);
    }

    /**
     * Returns the current percentage of the maximum pitch of the camera in an
     * open area.
     *
     * @return The current camera altitude percentage.
     */
    public int getPitch() {
        return (int) ((methods.client.getCameraPitch() - 1024) / 20.48);
    }

    /**
     * Returns the current x position of the camera.
     *
     * @return The x position.
     */
    public int getX() {
        return methods.client.getCamPosX();
    }

    /**
     * Returns the current y position of the camera.
     *
     * @return The y position.
     */
    public int getY() {
        return methods.client.getCamPosY();
    }

    /**
     * Returns the current z position of the camera.
     *
     * @return The z position.
     */
    public int getZ() {
        return methods.client.getCamPosZ();
    }
}
